﻿using System.Web;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Context;
using System;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using NHibernate.Mappings;
using System.Security.Principal;
using System.Globalization;
using System.IO;
using System.Web.Security;
using NHibernate.Dialect;
using NHibernate.Dialect.Function;
using NHibernate.Engine;
using NHibernate.Persister.Entity;
using NHibernate.Proxy;
using NHibernate.Mapping;

namespace Apollo
{
    public partial class Application : HttpApplication
    {
        protected static log4net.ILog log = log4net.LogManager.GetLogger(typeof(Application));
        protected static IList<LoadedAssembly> assemblies = new List<LoadedAssembly>();
        protected static ApplicationState state = ApplicationState.None;

        public static int ProcentLoaded { get; set; }
        public static NHibernate.Cfg.Configuration Configuration;
		public static ISessionFactory SessionFactory;
        public DateTime InitDateTime { get; set; }

        public static ISession CurrentSession {
            get {
                if (state != ApplicationState.Loaded && state != ApplicationState.PendingCache) 
                    Current.Initialize();

                return SessionFactory.GetCurrentSession(); 
            }
        }
        public static Apollo.Configuration.ApolloSection ConfigurationSection;

        public Application()
		{
            this.Modules = new List<IModule>();
            this.Actions = new List<Action>();

            // Configure log4net
            log4net.Config.XmlConfigurator.Configure();

            // Configure Apollo
            Apollo.Application.Configure();
                
            // Open/close ISession on each request
            this.BeginRequest += new System.EventHandler(Application_BeginRequest);
            this.EndRequest += new System.EventHandler(Application_EndRequest);
            this.AuthenticateRequest += new EventHandler(Application_AuthenticateRequest);
            this.Error += new EventHandler(Application_Error);
		}

        public static Application Current { get { return HttpContext.Current.ApplicationInstance as Application; } }

        public static IUser CurrentUser
        {
            get
            {
                IUser user = HttpContext.Current.User.Identity as IUser;
                Type userType = Apollo.Application.ConfigurationSection.Apollo.UserType ?? typeof(User);

                if (user != null && user.Role != null && user.Role.Name == Role.ADMIN_ROLE && user.Impersonate != null) {
                    if (user.Impersonate.GetType() != userType) 
                        return CurrentSession.Get(userType, user.Impersonate.Id) as IUser;
                    else
                        return user.Impersonate;
                }

                return user;
            }
        }

        #region Init Application 
        
        protected ApplicationState BeginInitialize() {
            if (state == ApplicationState.None)
            {
                state = ApplicationState.Pending;
                return ApplicationState.None;
            }

            return state;
        }

        protected void RollbackInitialize() {
            state = ApplicationState.None;

            log.Error("Application init failed!");
        }

        protected void CommitInitialize()
        {
            // open the session for the current context so the cache can load
            ManagedWebSessionContext.Bind(HttpContext.Current, SessionFactory.OpenSession());

            state = ApplicationState.PendingCache;

            // when the application is not installed this part will fail
            try { 
                
                LoadApplicationMeta();

                this.CheckModuleConsistancy();

            }
            catch { }

            state = ApplicationState.Loaded;

            this.CheckLog4NetConfiguration();

            log.Error("Application init completed! Duration: " + (DateTime.Now - this.InitDateTime).ToString());
        }

        protected void CheckLog4NetConfiguration() {
            try
            {
                string nhConnectionString = Configuration.Properties.Where(kv => kv.Key == "connection.connection_string")
                    .SingleOrDefault().Value;

                log4net.Repository.Hierarchy.Hierarchy hier = log4net.LogManager.GetRepository() as log4net.Repository.Hierarchy.Hierarchy;

                if (hier != null)
                {
                    log4net.Appender.AdoNetAppender adoAppender = hier.GetAppenders()
                        .SingleOrDefault(a => a.Name == "AdoNetAppender") as log4net.Appender.AdoNetAppender;
                    if (adoAppender != null)
                    {
                        if (nhConnectionString != adoAppender.ConnectionString)
                        {
                            adoAppender.ConnectionString = nhConnectionString;
                            adoAppender.ActivateOptions(); //refresh settings of appender

                            LogError(new ArgumentException("log4net connection string is invalid! It was replaced by NHibernate connection string."));
                        }
                    }
                }
            }
            catch (Exception ex) {
                LogError(ex);
            }
        }

        public ApplicationState InitializeAsync() {
            if (this.BeginInitialize() != ApplicationState.None)
                return state;

            this.InitDateTime = DateTime.Now;
            //log.Error("Application init started");

            // do the initialize
            try
            {
                // Configure NHibernate
                Configuration = new NHibernate.Cfg.Configuration();

                assemblies = this.LoadAssemblies();

                int numberOfLoadedAssemblies = 0;
                // Create NH xmls from mappings: 0 - 60 %
                foreach (Apollo.LoadedAssembly assembly in assemblies)
                {
                    NHibernate.Mappings.HbmSerializer.Instance.AddAssembly(assembly.Assembly);

                    numberOfLoadedAssemblies++;
                    Apollo.Application.ProcentLoaded = 60 * numberOfLoadedAssemblies / assemblies.Count;
                }


                // Add mappings to configuration: 60 - 100 %
                int numberOfLoadedXMLs = 0;
                foreach (string xml in NHibernate.Mappings.HbmSerializer.Instance.Xmls)
                {
                    Configuration.AddXml(xml);

                    numberOfLoadedXMLs++;
                    Apollo.Application.ProcentLoaded = 60 + (40 * numberOfLoadedXMLs
                        / NHibernate.Mappings.HbmSerializer.Instance.Xmls.Count);
                }

                Configuration.SetInterceptor(new NoLockInterceptor());

                // Build session factory
                SessionFactory = Configuration.BuildSessionFactory();

                
                // This does the actual initializing
                Apollo.Application.Current.CommitInitialize();
            }
            catch (Exception ex)
            {
                // This will set the state to NONE, letting another process init the application
                Apollo.Application.Current.RollbackInitialize();

                LogError(ex);
            }

            return state;
        }

        public void Initialize() {
            int maxTrials = 20;
            int currentTrials = 0;

            while (this.InitializeAsync() != ApplicationState.Loaded) {
                currentTrials++;

                if (currentTrials >= maxTrials)
                    throw new Exception("Failed to initialize!");

                System.Threading.Thread.Sleep(2000);
            }
        }

        protected IList<Apollo.LoadedAssembly> LoadAssemblies()
        {
            IList<Apollo.LoadedAssembly> assemblies = new List<Apollo.LoadedAssembly>();

            foreach (string dllPath in Directory.GetFiles(HttpRuntime.BinDirectory))
            {
                if (!dllPath.EndsWith(".dll"))
                    continue;

                try
                {
                    // load the assembly from actual location
                    Assembly assembly = Assembly.LoadFrom(dllPath);

                    // check if it's an Apollo module assembly
                    object[] assemblyAttrs = assembly.GetCustomAttributes(typeof(ApolloAttribute), true);
                    if (assemblyAttrs == null)
                        continue;
                    else if (assemblyAttrs.Length <= 0)
                        continue;
                    else
                    {
                        ApolloAttribute attr = (ApolloAttribute)assemblyAttrs[0];

                        assemblies.Add(new Apollo.LoadedAssembly()
                        {
                            Assembly = assembly,
                            Module = attr.Module
                        });
                    }
                }
                catch (Exception ex)
                {
                    LogError("Failed to load assembly from path: " + dllPath, ex);
                }
            }

            return assemblies;
        }

        #endregion

        #region Request

        protected void Application_BeginRequest(object sender, System.EventArgs e)
        {
            if (state != ApplicationState.Loaded)
            {
                // load the page
                HttpApplication context = sender as HttpApplication;

                if (context == null)
                    return;

                // ignore webservices
                if (context.Request.Url.AbsolutePath.ToLower().Contains("/webservices/"))
                    return;

                // goto Init page
                if (context.Request.Url.AbsolutePath.ToLower().EndsWith(".aspx") &&
                    !context.Request.Url.AbsolutePath.ToLower().Contains("/init/")) {
                        context.Context.RewritePath("~/Init/Default.aspx");
                }

                return;
            }

            ManagedWebSessionContext.Bind(HttpContext.Current, SessionFactory.OpenSession());
        }

        protected void Application_EndRequest(object sender, System.EventArgs e)
        {
            if (state != ApplicationState.Loaded)
                return;

            ISession session = ManagedWebSessionContext.Unbind(HttpContext.Current, SessionFactory);

            if (session == null)
                return;

            if (session.Transaction.IsActive)
            {
                session.Transaction.Rollback();
            }

            if (session != null)
            {
                session.Close();
            }
        }

        protected void Application_AuthenticateRequest(object sender, EventArgs e)
        {
            if (state != ApplicationState.Loaded)
                return;
            
            try
            {
                if (this.User != null && this.User.Identity != null && this.User.Identity.IsAuthenticated)
                {
                    // replace the current Identity with an Apollo.User
                    Type userType = Apollo.Application.ConfigurationSection.Apollo.UserType ?? typeof(User);

                    ICriteria crit = CurrentSession.CreateCriteria(userType, "this");
                    crit.Add(NHibernate.Criterion.Expression.Eq("this.Name", this.User.Identity.Name));
                    IUser user = crit.UniqueResult() as IUser;

                    if (user != null)
                    {
                        if (!user.IsActive)
                        {
                            FormsAuthentication.SignOut();
                            return;
                        }

                        user.IsAuthenticated = true;

                        this.Context.User = new GenericPrincipal(user, new string[] { });

                        if (user.LastLoginDate != DateTime.Today)
                        {
                            using (ITransaction tran = Apollo.Application.CurrentSession.BeginTransaction())
                                try
                                {
                                    user.LastLoginDate = DateTime.Today;

                                    Apollo.Application.CurrentSession.Save(user);

                                    tran.Commit();
                                }
                                catch
                                {
                                    tran.Rollback();
                                }
                        }
                    }
                    else {
                        FormsAuthentication.SignOut();
                    }
                }
            }
            catch (Exception ex) {
                LogWarning(ex);
            }
        }

        protected void Application_Error(object sender, EventArgs e)
        {
            LogError(Server.GetLastError());
        }

        #endregion

        #region Language and internationalization

        public static Language DefaultLanguage {
            get {
                string cacheKey = "APOLLO_CACHE_DEFAULT_LANGUAGE";

                if (HttpContext.Current.Application[cacheKey] != null)
                    return (Language)HttpContext.Current.Application[cacheKey];

                Language language = CurrentSession.QueryOver<Language>()
                                .Where(l => l.IsDefault == true)
                                .List().FirstOrDefault<Language>();

                if (language != null)
                    HttpContext.Current.Application.Add(cacheKey, language);

                return language;
            }
        }

        public static Language CurrentLanguage { get {
            if (HttpContext.Current.User.Identity is User)
                return (HttpContext.Current.User.Identity as User).Language;
            else
                return DefaultLanguage;
        } }

        public static string Serialize(object obj) {
            return Serialize(obj, false);
        }

        public static string Serialize(object obj, bool useGridFormats) {
            if (obj == null)
                return "";

            Type t = obj.GetType();

            // ignore invalid casts
            if (t == typeof(string))
                return (string)obj;

            if (useGridFormats)
            { // use grid formats
                if(t == typeof(int)) {
                    if (!string.IsNullOrEmpty(CurrentLanguage.IntGridFormat))
                        return Serialize(obj, CurrentLanguage.IntGridFormat);
                }
                else if((new Type[] { typeof(float?), typeof(float), typeof(decimal?), typeof(decimal), typeof(double?), typeof(double)}).Contains(t)){
                        if(!string.IsNullOrEmpty(CurrentLanguage.DecimalGridFormat))
                            return Serialize(obj, CurrentLanguage.DecimalGridFormat);
                }
            }
            
            if(t == typeof(int)) {
                if (!string.IsNullOrEmpty(CurrentLanguage.IntFormat))
                    return Serialize(obj, CurrentLanguage.IntFormat);
            }
            else if((new Type[] { typeof(float?), typeof(float), typeof(decimal?), typeof(decimal), typeof(double?), typeof(double)}).Contains(t)){
                if (!string.IsNullOrEmpty(CurrentLanguage.DecimalFormat))
                    return Serialize(obj, CurrentLanguage.DecimalFormat);
            }
            else if((new Type[] { typeof(DateTime?), typeof(DateTime) }).Contains(t)) {
                if (!string.IsNullOrEmpty(CurrentLanguage.DateTimeFormat))
                    return Serialize(obj, CurrentLanguage.DateTimeFormat);
            }

            return Serialize(obj, null as string);
        }

        public static string Serialize(object obj, string formatString) {
            if (obj == null)
                return "";

            Type t = obj.GetType();

            // ignore invalid casts
            if (t == typeof(string))
                return (string)obj;

            if (!string.IsNullOrEmpty(formatString)) {
                if (t == typeof(int))
                {
                    return ((int)obj).ToString(formatString, CultureInfo.InvariantCulture);
                }
                else if ((new Type[] { typeof(float?), typeof(float), typeof(decimal?), typeof(decimal), typeof(double?), typeof(double) }).Contains(t))
                {
                    return ((double)obj).ToString(formatString, CultureInfo.InvariantCulture);
                }
                else if ((new Type[] { typeof(DateTime?), typeof(DateTime) }).Contains(t))
                {
                    return ((DateTime)obj).ToString(formatString, CultureInfo.InvariantCulture);
                }
            }

            return obj.ToString();
        }

        public static string SerializeAndEncode(object obj) {
            string serialized = Serialize(obj);

            if (!string.IsNullOrEmpty(serialized))
                return serialized.Replace("\\\\", "\\").Replace("\"", "\\\"")
                    .Replace("\n", "").Replace("\r", " ").Replace("\t", " ");
            else
                return serialized;
        }

        public static object Deserialize(string objectString, Type t) {
            // ignore invalid casts
            if (t == typeof(string)) {
                if (string.IsNullOrEmpty(objectString))
                    return null;
                return objectString;
            }

            if (t == typeof(int))
            {
                try { return int.Parse(objectString, CultureInfo.InvariantCulture); }
                catch { return int.MinValue; }
            } 
            else if (t == typeof(int?))
            {
                try { return int.Parse(objectString, CultureInfo.InvariantCulture); }
                catch { return null; }
            }

            if (t == typeof(float)) 
                return float.Parse(objectString, CultureInfo.InvariantCulture);

            if (t == typeof(decimal))
                return decimal.Parse(objectString, CultureInfo.InvariantCulture);

            if (t == typeof(double))
                return double.Parse(objectString, CultureInfo.InvariantCulture);
            else if (t == typeof(double?)) {
                try { return double.Parse(objectString, CultureInfo.InvariantCulture); }
                catch { return null; }
            }

            if (t == typeof(DateTime) || t == typeof(DateTime?))
            {
                if (string.IsNullOrEmpty(CurrentLanguage.DateTimeFormat))
                    return DateTime.Parse(objectString);
                else
                    return DateTime.ParseExact(objectString, CurrentLanguage.DateTimeFormat, CultureInfo.InvariantCulture);
            }

            if (t == typeof(bool) || t == typeof(bool?))
            {
                if (objectString.ToLower() == "true" || objectString == "1")
                    return true;
                else
                    return false;
            }

            return objectString;
        }
                
        #endregion

        #region Configuration

        public static void Configure() {
            Apollo.Application.ConfigurationSection = System.Configuration.ConfigurationManager.GetSection("apollo-configuration")
                as Apollo.Configuration.ApolloSection;
        }

        #endregion

        #region Is dirty
        public bool IsDirtyEntity(Object entity)
        {
            ISessionImplementor sessionImpl = CurrentSession.GetSessionImplementation();

            IPersistenceContext persistenceContext = sessionImpl.PersistenceContext;

            EntityEntry oldEntry = persistenceContext.GetEntry(entity);

            String className = oldEntry.EntityName;

            IEntityPersister persister = sessionImpl.Factory.GetEntityPersister(className);



            if ((oldEntry == null) && (entity is INHibernateProxy))
            {
                INHibernateProxy proxy = entity as INHibernateProxy;

                Object obj = sessionImpl.PersistenceContext.Unproxy(proxy);

                oldEntry = sessionImpl.PersistenceContext.GetEntry(obj);
            }



            Object[] oldState = oldEntry.LoadedState;

            Object[] currentState = persister.GetPropertyValues(entity, sessionImpl.EntityMode);
            Int32[] dirtyProps = oldState.Select((o, i) => (oldState[i] == currentState[i]) ? -1 : i).Where(x => x >= 0).ToArray();

            return (dirtyProps != null);

        }

        public bool IsDirtyProperty(Object entity, String propertyName)
        {

            ISessionImplementor sessionImpl = CurrentSession.GetSessionImplementation();

            IPersistenceContext persistenceContext = sessionImpl.PersistenceContext;

            EntityEntry oldEntry = persistenceContext.GetEntry(entity);

            String className = oldEntry.EntityName;

            IEntityPersister persister = sessionImpl.Factory.GetEntityPersister(className);



            if ((oldEntry == null) && (entity is INHibernateProxy))
            {

                INHibernateProxy proxy = entity as INHibernateProxy;

                Object obj = sessionImpl.PersistenceContext.Unproxy(proxy);

                oldEntry = sessionImpl.PersistenceContext.GetEntry(obj);

            }



            Object[] oldState = oldEntry.LoadedState;

            Object[] currentState = persister.GetPropertyValues(entity, sessionImpl.EntityMode);

            Int32[] dirtyProps = persister.FindDirty(currentState, oldState, entity, sessionImpl);

            Int32 index = System.Array.IndexOf(persister.PropertyNames, propertyName);



            Boolean isDirty = (dirtyProps != null) ? (System.Array.IndexOf(dirtyProps, index) != -1) : false;



            return (isDirty);

        }

        public Object GetOriginalEntity(Object entity) {
            ISessionImplementor sessionImpl = CurrentSession.GetSessionImplementation();

            IPersistenceContext persistenceContext = sessionImpl.PersistenceContext;

            EntityEntry oldEntry = persistenceContext.GetEntry(entity);
            
            String className = oldEntry.EntityName;

            IEntityPersister persister = sessionImpl.Factory.GetEntityPersister(className);

            if ((oldEntry == null) && (entity is INHibernateProxy)) {

                INHibernateProxy proxy = entity as INHibernateProxy;

                Object obj = sessionImpl.PersistenceContext.Unproxy(proxy);

                oldEntry = sessionImpl.PersistenceContext.GetEntry(obj);

            }

            object orig = Activator.CreateInstance(entity.GetType());
            
            foreach (PropertyInfo pi in entity.GetType().GetProperties()) {
                if (pi.Name == persister.IdentifierPropertyName)
                    continue; // ignore identifier
                else if (pi.GetCustomAttributes(typeof(PropertyAttribute), true).Count() > 0
                    || pi.GetCustomAttributes(typeof(BelongsToAttribute), true).Count() > 0) {
                        pi.SetValue(orig, this.GetOriginalEntityProperty(entity, pi.Name), null);
                }
            }

            return orig;
        }

        public Object GetOriginalEntityProperty(Object entity, String propertyName)
        {

            ISessionImplementor sessionImpl = CurrentSession.GetSessionImplementation();

            IPersistenceContext persistenceContext = sessionImpl.PersistenceContext;

            EntityEntry oldEntry = persistenceContext.GetEntry(entity);

            String className = oldEntry.EntityName;

            IEntityPersister persister = sessionImpl.Factory.GetEntityPersister(className);


            if ((oldEntry == null) && (entity is INHibernateProxy))

            {

                INHibernateProxy proxy = entity as INHibernateProxy;

                Object obj = sessionImpl.PersistenceContext.Unproxy(proxy);

                oldEntry = sessionImpl.PersistenceContext.GetEntry(obj);

            }

 

            Object [] oldState = oldEntry.LoadedState;

            Object [] currentState = persister.GetPropertyValues(entity, sessionImpl.EntityMode);

            Int32 [] dirtyProps = persister.FindDirty(currentState, oldState, entity, sessionImpl);

            Int32 index = System.Array.IndexOf(persister.PropertyNames, propertyName);

 

            Boolean isDirty = (dirtyProps != null) ? (System.Array.IndexOf(dirtyProps, index) != -1) : false;

 

            return ((isDirty == true) ? oldState [ index ] : currentState [ index ]);

        }

        #endregion

        public enum ApplicationState { None, Pending, PendingCache, Loaded }

    }
}
